# coding=utf-8
from __future__ import print_function, absolute_import
from gm.api import *
import datetime
import numpy as np
import pandas as pd
import statsmodels.api as sm

# 策略中必须有init方法
def init(context):
    # 每天的09:30 定时执行algo任务
    schedule(schedule_func=algo, date_rule='1d', time_rule='09:31:00') 
    context.num = 100
    context.alpha_STR_history_num = 20# STR因子的历史回溯天数

def algo(context):
    # 当前时间str
    today = context.now.strftime("%Y-%m-%d %H:%M:%S")
    # 下一个交易日
    next_date = get_next_trading_date(exchange='SZSE', date=today)
    # 上一个交易日
    last_date = get_previous_trading_date(exchange='SHSE', date=context.now)
    # 每月最后一个交易日时换股
    if today[5:7]!=next_date[5:7]:
        # 获取全A股票（剔除停牌股和ST股）
        all_stocks,all_stocks_str = get_normal_stocks(context.now)
        # 计算STR因子（量稳换手率因子STR（即换手率的稳定性，The Stability of Turnover Rate））
        STR = get_alpha_STR(context,all_stocks_str,last_date,context.alpha_STR_history_num)
        # 获取最小因子的前N只股票
        trade_stocks = list(STR.replace([-np.inf,np.inf],np.nan).dropna().sort_values(ascending=True)[:context.num].index)
        print(context.now,'待买入股票：{}'.format(trade_stocks))

        ## 股票交易
        # 获取持仓
        positions = context.account().positions()
        # 卖出不在trade_stocks中的持仓(跌停不卖出)
        for position in positions:
            symbol = position['symbol']
            if symbol not in trade_stocks:
                price_limit = get_history_instruments(symbol, fields='lower_limit', start_date=context.now, end_date=context.now, df=True)
                new_price = history(symbol=symbol, frequency='60s', start_time=context.now, end_time=context.now, fields='close', df=True)
                if symbol not in trade_stocks and (len(new_price)==0 or len(price_limit)==0 or price_limit['lower_limit'][0]!=round(new_price['close'][0],2)):
                    # new_price为空时，是开盘后无成交的现象，此处忽略该情况，可能会包含涨跌停的股票
                    order_target_percent(symbol=symbol, percent=0, order_type=OrderType_Market, position_side=PositionSide_Long)
        # 买入股票（涨停不买入）
        for symbol in trade_stocks:
            price_limit = get_history_instruments(symbol, fields='upper_limit', start_date=context.now, end_date=context.now, df=True)
            new_price = history(symbol=symbol, frequency='60s', start_time=context.now, end_time=context.now, fields='close', df=True)
            if len(new_price)==0 or len(price_limit)==0 or price_limit['upper_limit'][0]!=round(new_price['close'][0],2):
                # new_price为空时，是开盘后无成交的现象，此处忽略该情况，可能会包含涨跌停的股票
                order_target_percent(symbol=symbol, percent=1/len(trade_stocks), order_type=OrderType_Market, position_side=PositionSide_Long)


def get_normal_stocks(date):
    """
    获取目标日期date的A股代码（剔除停牌股、ST股、次新股（一年期））
    :param date：目标日期
    """
    if isinstance(date,str):
        date = datetime.datetime.strptime(date,"%Y-%m-%d %H:%M:%S")
    df_code = get_instruments(sec_types=SEC_TYPE_STOCK, skip_suspended=True, skip_st=True, fields='symbol, sec_level, is_suspended, listed_date, delisted_date', df=True)
    all_stocks = [code for code in df_code[(df_code['sec_level']==1)&(df_code['is_suspended']==0)&(df_code['listed_date']<=date-datetime.timedelta(days=365))&(df_code['delisted_date']>date)].symbol.to_list() if code[:6]!='SHSE.9' and code[:6]!='SZSE.2']
    all_stocks_str = ','.join(all_stocks)
    return all_stocks,all_stocks_str

def get_alpha_STR(context,security,date,counts=1):
    """计算STR因子数据（包含date当日）
    :param security：目标股票，'***,***,***'格式
    :param date：目标日期
    :param counts：历史回溯天数
    """
    # 计算STR
    turnrate = get_fundamentals_n(table='trading_derivative_indicator', symbols=security, end_date=date, fields='TURNRATE',count=counts, df=True).set_index(['pub_date','symbol'])
    turnrate = turnrate[['TURNRATE']].drop_duplicates(keep='first').unstack()
    turnrate.columns = turnrate.columns.droplevel(level=0)
    turnrate = turnrate.fillna(method='ffill').iloc[-counts:,:]
    STR = turnrate.std().dropna()
    # 去极值
    STR = winsorize_med(STR)
    # 标准化
    STR = standardlize(STR)
    # 中性化
    STR = neutralize_MarketValue(context,STR,date)
    return STR

def winsorize_med(data, scale=3, inclusive=True, inf2nan=True):
    """
    去极值
    :param data：待处理数据[Series]
    :param scale：标准差倍数，默认为3
    :param inclusive：True为将边界外的数值调整为边界值，False为将边界外的数值调整为NaN
    :param inf2nan：True为将inf转化为nan，False不转化
    """
    data = data.astype('float')
    if inf2nan:
        data = data.replace([np.inf, -np.inf], np.nan)
    std_ = data.std()
    mean_ = data.mean()
    if inclusive:
        data[data>mean_+std_*scale]=mean_+std_*scale
        data[data<mean_-std_*scale]=mean_-std_*scale
    else:
        data[data>mean_+std_*scale]=np.nan
        data[data<mean_-std_*scale]=np.nan
    return data

def standardlize(data, inf2nan=True):
    """
    标准化
    :param data:待处理数据
    :param inf2nan：是否将inf转化为nan
    """
    if inf2nan:
        data = data.replace([np.inf, -np.inf], np.nan)
    return (data - data.mean()) / data.std()
    
def neutralize_MarketValue(context,data,date,counts=1):
    """
    市值中性化
    :param data:待处理数据
    :param date:目标日期
    :param counts：历史回溯天数
    """
    if isinstance(data,pd.Series):
        data = data.to_frame()
    security = data.index.to_list()
    market_value = get_fundamentals_n(table='trading_derivative_indicator', symbols=security, end_date=date, fields='TOTMKTCAP',count=counts, df=True)
    max_date = market_value['pub_date'].max()
    market_value = market_value[market_value['pub_date']==max_date][['symbol','TOTMKTCAP']].set_index('symbol')
    x = sm.add_constant(market_value)
    common_index = list(set(x.index) & set(data.index))
    x = x.loc[common_index,:]
    data = data.loc[common_index,:]
    residual = sm.OLS(data, x).fit().resid# 此处使用最小二乘回归计算
    return residual
    
if __name__ == '__main__':
    '''
        strategy_id策略ID, 由系统生成
        filename文件名, 请与本文件名保持一致
        mode运行模式, 实时模式:MODE_LIVE回测模式:MODE_BACKTEST
        token绑定计算机的ID, 可在系统设置-密钥管理中生成
        backtest_start_time回测开始时间
        backtest_end_time回测结束时间
        backtest_adjust股票复权方式, 不复权:ADJUST_NONE前复权:ADJUST_PREV后复权:ADJUST_POST
        backtest_initial_cash回测初始资金
        backtest_commission_ratio回测佣金比例
        backtest_slippage_ratio回测滑点比例
        '''
    run(strategy_id='adbd86dd-5d90-11ec-9850-7085c223669d',
        filename='main.py',
        mode=MODE_BACKTEST,
        token='{{token}}',
        backtest_start_time='2020-12-31 08:00:00',
        backtest_end_time='2021-12-22 16:00:00',
        backtest_adjust=ADJUST_PREV,
        backtest_initial_cash=10000000,
        backtest_commission_ratio=0.0016,# 买入万三手续费+卖出万三手续费和千1印花税，免5
        backtest_slippage_ratio=0.00246)